var net = require("net");
var util = require("util");
var fs = require("fs");
var dummyfs = require("./dummyfs");
var PathModule = require("path");
var glob = require('./glob');
require('./date-format');

var createServer = function createServer(host, sandbox) {
    var server = net.createServer();
    server.baseSandbox = sandbox;
    server.debugging = 0;

    var logIf = function(level, message, socket) {
        if (server.debugging >= level) {
            if (socket)
                console.log(socket.remoteAddress + ": " + message);
            else
                console.log(message);
        }
    };

    server.on("listening", function() {
        logIf(0, "nodeFTPd server up and ready for connections");
    });
    server.on("connection", function(socket) {
        server.emit("client:connected", socket);

        socket.setTimeout(0);
        socket.setEncoding("ascii");
        socket.setNoDelay();
        socket.passive = false;
        socket.dataHost = null;
        socket.dataPort = 20;
        socket.dataListener = null;
        socket.dataSocket = null;
        socket.mode = "ascii";
        socket.filefrom = "";
        socket.authFailures = 0;
        socket.username = null;

        socket.sandbox = sandbox;
        socket.fs = new dummyfs.dummyfs('/');
        logIf(0, "Base FTP directory: " + socket.fs.cwd());


        var authenticated = function() {
            return (socket.username ? true : false);
        };

        var authFailures = function() {
            if (socket.authFailures >= 2) {
                socket.end();
                return true;
            }
            return false;
        };

        var closeDataConnections = function() {
            if (socket.dataListener) socket.dataListener.close();
            if (socket.dataSocket) socket.dataSocket.end();
        };


        var whenDataWritable = function(callback) {
            if (socket.passive) {
                if (socket.dataSocket) {
                    logIf(3, "A data connection exists", socket);
                    if (callback) callback(socket.dataSocket);
                } else {
                    logIf(3, "Passive, but no data socket exists ... waiting", socket);
                    socket.dataListener.on('data-ready', function(dataSocket) {
                        logIf(3, "Looks like waiting paid off. Here we go!");
                        callback(dataSocket);
                    });
                }
            } else {
                if (socket.dataSocket) {
                    logIf(3, "Using existing non-passive dataSocket", socket);
                    callback(socket.dataSocket);
                } else {
                    logIf(1, "Opening data connection to " + socket.dataHost + ":" + socket.dataPort, socket);
                    var dataSocket = new net.Socket();
                    dataSocket.on("data", function(data) {
                        logIf(3, dataSocket.remoteAddress + ' event: data ; ' + (Buffer.isBuffer(data) ? 'buffer' : 'string'));
                    });
                    dataSocket.addListener("connect", function() {
                        logIf(3, "Data connection succeeded", socket);
                        dataSocket.pause();
                        socket.dataSocket = dataSocket;
                        logIf(3, "Data connection succeeded", socket);
                        callback(dataSocket);
                    });
                    dataSocket.addListener("close", function(had_error) {
                        socket.dataSocket = null;
                        if (had_error)
                            logIf(0, "Data event: close due to error", socket);
                        else
                            logIf(3, "Data event: close", socket);
                    });
                    dataSocket.addListener("end", function() {
                        logIf(3, "Data event: end", socket);
                    });
                    dataSocket.addListener("error", function(err) {
                        logIf(0, "Data event: error: " + err, socket);
                        dataSocket.destroy();
                    });
                    dataSocket.connect(socket.dataPort, socket.dataHost);
                }
            }
        };

        socket.addListener("data", function(data) {
            data = (data + '').trim();
            logIf(2, "FTP command: " + data, socket);

            var command, commandArg;
            var index = data.indexOf(" ");
            if (index > 0) {
                command = data.substring(0, index).trim().toUpperCase();
                commandArg = data.substring(index + 1, data.length).trim();
            } else {
                command = data.trim().toUpperCase();
                commandArg = '';
            }

            switch (command) {
                case "CDUP":
                    if (!authenticated()) break;
                    socket.write("250 Directory changed to " + socket.fs.chdir("..") + "\r\n");
                    break;
                case "CWD":
                    if (!authenticated()) break;
                    var path = PathModule.join(socket.sandbox, PathModule.resolve(socket.fs.cwd(), commandArg));
                    fs.exists(path, function(exists) {
                        if (!exists) {
                            socket.write("550 Folder not found.\r\n");
                            return;
                        }
                        socket.write("250 CWD successful. \"" + socket.fs.chdir(commandArg) + "\" is current directory\r\n");
                    });
                    break;
                case "DELE":
                    if (!authenticated()) break;
                    var filename = PathModule.resolve(socket.fs.cwd(), commandArg);
                    fs.unlink(PathModule.join(socket.sandbox, filename), function(err) {
                        if (err) {
                            logIf(0, "Error deleting file: " + filename + ", " + err, socket);
                            socket.write("550 Permission denied\r\n");
                        } else
                            socket.write("250 File deleted\r\n");
                    });
                    break;

                case "FEAT":
                    socket.write("211-Features\r\n");
                    socket.write(" SIZE\r\n");
                    socket.write("211 end\r\n");
                    break;
                case "LIST":
                    if (!authenticated()) break;

                    whenDataWritable(function(dataSocket) {
                        var leftPad = function(text, width) {
                            var out = '';
                            for (var j = text.length; j < width; j++) out += ' ';
                            out += text;
                            return out;
                        };
                        var success = function() {
                            socket.write("226 Transfer OK\r\n");
                            dataSocket.end();
                        };
                        var failure = function() {
                            dataSocket.end();
                        };
                        var path = PathModule.join(socket.sandbox, socket.fs.cwd());
                        if (dataSocket.readable) dataSocket.resume();
                        logIf(3, "Sending file list", socket);
                        fs.readdir(path, function(err, files) {
                            if (err) {
                                logIf(0, "While sending file list, reading directory: " + err, socket);
                                dataSocket.write("", failure);
                            } else {
                                socket.write("150 Here comes the directory listing\r\n", function() {
                                    logIf(3, "Directory has " + files.length + " files", socket);
                                    for (var i = 0; i < files.length; i++) {
                                        var file = files[i];
                                        var s = fs.statSync(PathModule.join(path, file));
                                        var line = s.isDirectory() ? 'd' : '-';
                                        if (i > 0) dataSocket.write("\r\n");
                                        line += (0400 & s.mode) ? 'r' : '-';
                                        line += (0200 & s.mode) ? 'w' : '-';
                                        line += (0100 & s.mode) ? 'x' : '-';
                                        line += (040 & s.mode) ? 'r' : '-';
                                        line += (020 & s.mode) ? 'w' : '-';
                                        line += (010 & s.mode) ? 'x' : '-';
                                        line += (04 & s.mode) ? 'r' : '-';
                                        line += (02 & s.mode) ? 'w' : '-';
                                        line += (01 & s.mode) ? 'x' : '-';
                                        line += " 1 ftp ftp ";
                                        line += leftPad(s.size.toString(), 12) + ' ';
                                        var d = new Date(s.mtime);
                                        line += leftPad(d.format('M d H:i'), 12) + ' '; // need to use a date string formatting lib
                                        line += file;
                                        dataSocket.write(line);
                                    }
                                    dataSocket.write("\r\n", success);
                                });
                            }
                        });
                    });
                    break;

                case "MKD":
                    if (!authenticated()) break;
                    var filename = PathModule.resolve(socket.fs.cwd(), commandArg);
                    fs.mkdir(PathModule.join(socket.sandbox, filename), 0755, function(err) {
                        if (err) {
                            logIf(0, "Error making directory " + filename + " because " + err, socket);
                            socket.write("550 \"" + filename + "\" directory NOT created\r\n");
                            return;
                        }
                        socket.write("257 \"" + filename + "\" directory created\r\n");
                    });
                    break;
                case "NLST":
                    if (!authenticated()) break;
                    whenDataWritable(function(dataSocket) {
                        var success = function() {
                            socket.write("226 Transfer OK\r\n");
                            dataSocket.end();
                        };
                        var failure = function() {
                            dataSocket.end();
                        };
                        var temp = '';
                        if (commandArg) {
                            commandArg = commandArg.replace(/\/{2,}|\.{2}/g, '');
                            if (commandArg.substr(0, 1) == '/') {
                                temp = PathModule.join(socket.sandbox, commandArg);
                            } else {
                                temp = PathModule.join(socket.sandbox, socket.fs.cwd(), commandArg);
                            }
                        } else temp = PathModule.join(socket.sandbox, socket.fs.cwd());
                        if (dataSocket.readable) dataSocket.resume();
                        logIf(3, "Sending file list", socket);

                        glob.glob(temp, function(err, files) {
                            if (err) {
                                logIf(0, "During NLST, error globbing files: " + err, socket);
                                socket.write("451 Read error\r\n");
                                dataSocket.write("", failure);
                                return;
                            }
                            socket.write("150 Here comes the directory listing\r\n", function() {
                                logIf(3, "Directory has " + files.length + " files", socket);
                                dataSocket.write(files.map(PathModule.basename).join("\015\012") + "\015\012", success);
                            });
                        });
                    });
                    break;

                case "PASS":
                    socket.emit(
                        "command:pass",
                        commandArg,
                        function(username) {
                            socket.write("230 Logged on\r\n");
                            socket.username = username;
                            socket.sandbox = PathModule.join(server.baseSandbox, username);
                        },
                        function() {
                            socket.write("530 Invalid password\r\n");
                            socket.authFailures++;
                            socket.username = null;
                        }
                    );
                    break;
                case "USER":
                    socket.emit(
                        "command:user",
                        commandArg,
                        function() {
                            socket.write("331 Password required for " + commandArg + "\r\n");
                        },
                        function() {
                            socket.write("530 Invalid username: " + commandArg + "\r\n");
                        }
                    );
                    break;
                case "PASV":

                    if (!authenticated()) break;
                    if (socket.dataListener) socket.dataListener.close();
                    if (socket.dataSocket) socket.dataSocket.end();
                    socket.dataListener = null;
                    socket.dataSocket = null;
                    socket.pause();
                    var pasv = net.createServer(function(psocket) {
                        logIf(1, "Incoming passive data connection", socket);
                        psocket.pause();
                        psocket.on("data", function(data) {
                            logIf(4, 'Data event: received ' + (Buffer.isBuffer(data) ? 'buffer' : 'string'), socket);
                        });
                        psocket.on("end", function() {
                            logIf(3, "Passive data event: end", socket);
                            socket.dataSocket = null;
                            if (socket.readable) socket.resume();
                        });
                        psocket.addListener("error", function(err) {
                            logIf(0, "Passive data event: error: " + err, socket);
                            socket.dataSocket = null;
                            if (socket.readable) socket.resume();
                        });
                        psocket.addListener("close", function(had_error) {
                            logIf(
                                (had_error ? 0 : 3),
                                "Passive data event: close " + (had_error ? " due to error" : ""),
                                socket
                            );
                            if (socket.readable) socket.resume();
                        });


                        socket.dataSocket = psocket;

                        if (socket.readable) socket.resume();

                        socket.dataListener.emit("data-ready", psocket);

                    });

                    pasv.addListener("listening", function() {
                        var port = pasv.address().port;
                        socket.passive = true;
                        socket.dataHost = host;
                        socket.dataPort = port;
                        logIf(3, "Passive data connection listening on port " + port, socket);
                        var i1 = parseInt(port / 256);
                        var i2 = parseInt(port % 256);
                        socket.write("227 Entering Passive Mode (" + host.split(".").join(",") + "," + i1 + "," + i2 + ")\r\n");
                    });
                    pasv.on("close", function() {
                        logIf(3, "Passive data listener closed", socket);
                        if (socket.readable) socket.resume();
                    });
                    pasv.listen(0);
                    socket.dataListener = pasv;
                    logIf(3, "Passive data connection beginning to listen", socket);
                    break;

                case "PORT":

                    if (!authenticated()) break;
                    socket.passive = false;
                    socket.dataSocket = null;
                    var addr = commandArg.split(",");
                    socket.dataHost = addr[0] + "." + addr[1] + "." + addr[2] + "." + addr[3];
                    socket.dataPort = (parseInt(addr[4]) * 256) + parseInt(addr[5]);
                    socket.write("200 PORT command successful.\r\n");
                    break;
                case "PWD":

                    if (!authenticated()) break;
                    socket.write("257 \"" + socket.fs.cwd() + "\" is current directory\r\n");
                    break;
                case "QUIT":

                    socket.write("221 Goodbye\r\n");
                    socket.end();
                    closeDataConnections();
                    break;

                case "RETR":

                    whenDataWritable(function(dataSocket) {

                        dataSocket.setEncoding(socket.mode);
                        var filename = PathModule.resolve(socket.fs.cwd(), commandArg);
                        logIf(3, "DATA file " + filename + " opened");
                        var from = fs.createReadStream(PathModule.join(socket.sandbox, filename), { flags: "r" });
                        from.on("error", function() {
                            logIf(2, "Error reading file");
                        });
                        from.on("end", function() {
                            logIf(3, "DATA file " + filename + " closed");
                            dataSocket.end();
                            socket.write("226 Closing data connection\r\n");
                        });

                        logIf(3, "DATA file " + filename + " opened");
                        socket.write("150 Opening " + socket.mode.toUpperCase() + " mode data connection\r\n");

                        if (dataSocket.readable) {
                            dataSocket.resume();
                            from.pipe(dataSocket);
                        }
                    });
                    break;
                case "RMD":

                    if (!authenticated()) break;
                    var filename = PathModule.resolve(socket.fs.cwd(), commandArg);
                    fs.rmdir(PathModule.join(socket.sandbox, filename), function(err) {
                        if (err) {
                            logIf(0, "Error removing directory " + filename, socket);
                            socket.write("550 Delete operation failed\r\n");
                        } else
                            socket.write("250 \"" + filename + "\" directory removed\r\n");
                    });
                    break;
                case "RNFR":

                    if (!authenticated()) break;
                    socket.filefrom = PathModule.resolve(socket.fs.cwd(), commandArg);
                    logIf(3, "Rename from " + socket.filefrom, socket);
                    fs.exists(PathModule.join(socket.sandbox, socket.filefrom), function(exists) {
                        if (exists) socket.write("350 File exists, ready for destination name\r\n");
                        else socket.write("350 Command failed, file does not exist\r\n");
                    });
                    break;
                case "RNTO":

                    if (!authenticated()) break;
                    var fileto = PathModule.resolve(socket.fs.cwd(), commandArg);
                    fs.rename(PathModule.join(socket.sandbox, socket.filefrom), PathModule.join(socket.sandbox, fileto), function(err) {
                        if (err) {
                            logIf(3, "Error renaming file from " + socket.filefrom + " to " + fileto, socket);
                            socket.write("550 Rename failed\r\n");
                        } else
                            socket.write("250 File renamed successfully\r\n");
                    });
                    break;
                case "SIZE":

                    if (!authenticated()) break;
                    var filename = PathModule.resolve(socket.fs.cwd(), commandArg);
                    fs.stat(PathModule.join(socket.sandbox, filename), function(err, s) {
                        if (err) {
                            logIf(0, "Error getting size of file: " + filename, socket);
                            socket.write("450 Failed to get size of file\r\n");
                            return;
                        }
                        socket.write("213 " + s.size + "\r\n");
                    });
                    break;
                case "STOR":

                    if (!authenticated()) break;
                    whenDataWritable(function(dataSocket) {

                        filename = PathModule.resolve(socket.fs.cwd(), commandArg);
                        var destination = fs.createWriteStream(PathModule.join(socket.sandbox, filename), { flags: 'w+', mode: 0644 });
                        destination.on("error", function(err) {
                            logIf(0, 'Error opening/creating file: ' + filename, socket);
                            socket.write("553 Could not create file\r\n");
                            dataSocket.end();
                        });
                        destination.on("close", function() {});

                        logIf(3, "File opened/created: " + filename, socket);

                        dataSocket.addListener("end", function() {
                            socket.write("226 Data connection closed\r\n");
                        });
                        dataSocket.addListener("error", function(err) {
                            logIf(0, "Error transferring " + filename + ": " + err, socket);
                        });
                        logIf(3, "Told client ok to send file data", socket);
                        socket.write("150 Ok to send data\r\n");
                        if (dataSocket.readable) {
                            dataSocket.resume();
                            dataSocket.pipe(destination);
                        }
                    });
                    break;
                case "SYST":
                    socket.write("215 UNIX emulated by NodeFTPd\r\n");
                    break;
                case "TYPE":
                    if (!authenticated()) break;
                    if (commandArg == "A") {
                        socket.mode = "ascii";
                        socket.write("200 Type set to A\r\n");
                    } else {
                        socket.mode = "binary";
                        socket.write("200 Type set to I\r\n");
                    }
                    break;

                case "XPWD":
                    socket.write("257 " + socket.fs.cwd() + " is the current directory\r\n");
                    break;
                default:
                    socket.write("202 Not supported\r\n");
                    break;
            }

        });

        socket.addListener("end", function() {
            logIf(1, "Client connection ended", socket);
        });
        socket.addListener("error", function(err) {
            logIf(0, "Client connection error: " + err, socket);
        });

        logIf(1, "Connection", socket);
        socket.write("220 FTP server (nodeftpd) ready\r\n");
    });

    server.addListener("close", function() {
        logIf(0, "Server closed");
    });

    return server;
}
util.inherits(createServer, process.EventEmitter);
exports.createServer = createServer;